bot.Mouse = function (opt_state, opt_modifiersState, opt_eventEmitter) {
    goog.base(this, opt_modifiersState, opt_eventEmitter);

    this.buttonPressed_ = null;

    this.elementPressed_ = null;

    this.clientXY_ = new goog.math.Coordinate(0, 0);

    this.nextClickIsDoubleClick_ = false;

    this.hasEverInteracted_ = false;

    if (opt_state) {
        if (goog.isNumber(opt_state['buttonPressed'])) {
            this.buttonPressed_ = opt_state['buttonPressed'];
        }

        try {
            if (bot.dom.isElement(opt_state['elementPressed'])) {
                this.elementPressed_ = opt_state['elementPressed'];
            }
        } catch (ignored) {
            this.buttonPressed_ = null;
        }

        this.clientXY_ = new goog.math.Coordinate(opt_state['clientXY']['x'], opt_state['clientXY']['y']);

        this.nextClickIsDoubleClick_ = !!opt_state['nextClickIsDoubleClick'];
        this.hasEverInteracted_ = !!opt_state['hasEverInteracted'];

        try {
            if (opt_state['element'] && bot.dom.isElement(opt_state['element'])) {
                this.setElement(/** @type {!Element} */ (opt_state['element']));
            }
        } catch (ignored) {
            this.buttonPressed_ = null;
        }
    }
};
goog.inherits(bot.Mouse, bot.Device);

bot.Mouse.State;

bot.Mouse.Button = {
    LEFT: 0,
    MIDDLE: 1,
    RIGHT: 2,
};

bot.Mouse.NO_BUTTON_VALUE_INDEX_ = 3;

bot.Mouse.MOUSE_BUTTON_VALUE_MAP_ = (function () {
    // EventTypes can safely be used as keys without collisions in a JS Object,
    // because its toString method returns a unique string (the event type name).
    var buttonValueMap = {};
    if (bot.userAgent.IE_DOC_PRE9) {
        buttonValueMap[bot.events.EventType.CLICK] = [0, 0, 0, null];
        buttonValueMap[bot.events.EventType.CONTEXTMENU] = [null, null, 0, null];
        buttonValueMap[bot.events.EventType.MOUSEUP] = [1, 4, 2, null];
        buttonValueMap[bot.events.EventType.MOUSEOUT] = [0, 0, 0, 0];
        buttonValueMap[bot.events.EventType.MOUSEMOVE] = [1, 4, 2, 0];
    } else if (goog.userAgent.WEBKIT || bot.userAgent.IE_DOC_9) {
        buttonValueMap[bot.events.EventType.CLICK] = [0, 1, 2, null];
        buttonValueMap[bot.events.EventType.CONTEXTMENU] = [null, null, 2, null];
        buttonValueMap[bot.events.EventType.MOUSEUP] = [0, 1, 2, null];
        buttonValueMap[bot.events.EventType.MOUSEOUT] = [0, 1, 2, 0];
        buttonValueMap[bot.events.EventType.MOUSEMOVE] = [0, 1, 2, 0];
    } else {
        buttonValueMap[bot.events.EventType.CLICK] = [0, 1, 2, null];
        buttonValueMap[bot.events.EventType.CONTEXTMENU] = [null, null, 2, null];
        buttonValueMap[bot.events.EventType.MOUSEUP] = [0, 1, 2, null];
        buttonValueMap[bot.events.EventType.MOUSEOUT] = [0, 0, 0, 0];
        buttonValueMap[bot.events.EventType.MOUSEMOVE] = [0, 0, 0, 0];
    }

    if (bot.userAgent.IE_DOC_10) {
        buttonValueMap[bot.events.EventType.MSPOINTERDOWN] = buttonValueMap[bot.events.EventType.MOUSEUP];
        buttonValueMap[bot.events.EventType.MSPOINTERUP] = buttonValueMap[bot.events.EventType.MOUSEUP];
        buttonValueMap[bot.events.EventType.MSPOINTERMOVE] = [-1, -1, -1, -1];
        buttonValueMap[bot.events.EventType.MSPOINTEROUT] = buttonValueMap[bot.events.EventType.MSPOINTERMOVE];
        buttonValueMap[bot.events.EventType.MSPOINTEROVER] = buttonValueMap[bot.events.EventType.MSPOINTERMOVE];
    }

    buttonValueMap[bot.events.EventType.DBLCLICK] = buttonValueMap[bot.events.EventType.CLICK];
    buttonValueMap[bot.events.EventType.MOUSEDOWN] = buttonValueMap[bot.events.EventType.MOUSEUP];
    buttonValueMap[bot.events.EventType.MOUSEOVER] = buttonValueMap[bot.events.EventType.MOUSEOUT];
    return buttonValueMap;
})();

/**
 * Maps mouse events to corresponding MSPointer event.
 * @private {!Object.<bot.events.EventType, bot.events.EventType>}
 */
bot.Mouse.MOUSE_EVENT_MAP_ = (function () {
    var map = {};
    map[bot.events.EventType.MOUSEDOWN] = bot.events.EventType.MSPOINTERDOWN;
    map[bot.events.EventType.MOUSEMOVE] = bot.events.EventType.MSPOINTERMOVE;
    map[bot.events.EventType.MOUSEOUT] = bot.events.EventType.MSPOINTEROUT;
    map[bot.events.EventType.MOUSEOVER] = bot.events.EventType.MSPOINTEROVER;
    map[bot.events.EventType.MOUSEUP] = bot.events.EventType.MSPOINTERUP;
    return map;
})();

/**
 * Attempts to fire a mousedown event and then returns whether or not the
 * element should receive focus as a result of the mousedown.
 *
 * @param {?number=} opt_count Number of clicks that have been performed.
 * @return {boolean} Whether to focus on the element after the mousedown.
 * @private
 */
bot.Mouse.prototype.fireMousedown_ = function (opt_count) {
    // On some browsers, a mouse down event on an OPTION or SELECT element cause
    // the SELECT to open, blocking further JS execution. This is undesirable,
    // and so needs to be detected. We always focus in this case.
    // TODO: This is a nasty way to avoid locking the browser
    var isFirefox3 = goog.userAgent.GECKO && !bot.userAgent.isProductVersion(4);
    var blocksOnMousedown =
        (goog.userAgent.WEBKIT || isFirefox3) &&
        (bot.dom.isElement(this.getElement(), goog.dom.TagName.OPTION) || bot.dom.isElement(this.getElement(), goog.dom.TagName.SELECT));
    if (blocksOnMousedown) {
        return true;
    }

    // On some browsers, if the mousedown event handler makes a focus() call to
    // change the active element, this preempts the focus that would happen by
    // default on the mousedown, so we should not explicitly focus in this case.
    var beforeActiveElement;
    var mousedownCanPreemptFocus = goog.userAgent.GECKO || goog.userAgent.IE;
    if (mousedownCanPreemptFocus) {
        beforeActiveElement = bot.dom.getActiveElement(this.getElement());
    }
    var performFocus = this.fireMouseEvent_(bot.events.EventType.MOUSEDOWN, null, null, false, opt_count);
    if (performFocus && mousedownCanPreemptFocus && beforeActiveElement != bot.dom.getActiveElement(this.getElement())) {
        return false;
    }
    return performFocus;
};

bot.Mouse.prototype.pressButton = function (button, opt_count) {
    if (!goog.isNull(this.buttonPressed_)) {
        throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR, 'Cannot press more than one button or an already pressed button.');
    }
    this.buttonPressed_ = button;
    this.elementPressed_ = this.getElement();

    var performFocus = this.fireMousedown_(opt_count);
    if (performFocus) {
        if (
            bot.userAgent.IE_DOC_10 &&
            this.buttonPressed_ == bot.Mouse.Button.LEFT &&
            bot.dom.isElement(this.elementPressed_, goog.dom.TagName.OPTION)
        ) {
            this.fireMSPointerEvent(
                bot.events.EventType.MSGOTPOINTERCAPTURE,
                this.clientXY_,
                0,
                bot.Device.MOUSE_MS_POINTER_ID,
                MSPointerEvent.MSPOINTER_TYPE_MOUSE,
                true,
            );
        }
        this.focusOnElement();
    }
};

bot.Mouse.prototype.releaseButton = function (opt_force, opt_count) {
    if (goog.isNull(this.buttonPressed_)) {
        throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR, 'Cannot release a button when no button is pressed.');
    }

    this.maybeToggleOption();

    // If a mouseup event is dispatched to an interactable event, and that mouseup
    // would complete a click, then the click event must be dispatched even if the
    // element becomes non-interactable after the mouseup.
    var elementInteractableBeforeMouseup = bot.dom.isInteractable(this.getElement());
    this.fireMouseEvent_(bot.events.EventType.MOUSEUP, null, null, opt_force, opt_count);

    try {
        // https://github.com/SeleniumHQ/selenium/issues/1509
        // TODO: Middle button can also trigger click.
        if (this.buttonPressed_ == bot.Mouse.Button.LEFT && this.getElement() == this.elementPressed_) {
            if (!(bot.userAgent.WINDOWS_PHONE && bot.dom.isElement(this.elementPressed_, goog.dom.TagName.OPTION))) {
                this.clickElement(
                    this.clientXY_,
                    this.getButtonValue_(bot.events.EventType.CLICK),
                    /* opt_force */ elementInteractableBeforeMouseup,
                );
            }
            this.maybeDoubleClickElement_();
            if (
                bot.userAgent.IE_DOC_10 &&
                this.buttonPressed_ == bot.Mouse.Button.LEFT &&
                bot.dom.isElement(this.elementPressed_, goog.dom.TagName.OPTION)
            ) {
                this.fireMSPointerEvent(
                    bot.events.EventType.MSLOSTPOINTERCAPTURE,
                    new goog.math.Coordinate(0, 0),
                    0,
                    bot.Device.MOUSE_MS_POINTER_ID,
                    MSPointerEvent.MSPOINTER_TYPE_MOUSE,
                    false,
                );
            }
            // TODO: In Linux, this fires after mousedown event.
        } else if (this.buttonPressed_ == bot.Mouse.Button.RIGHT) {
            this.fireMouseEvent_(bot.events.EventType.CONTEXTMENU);
        }
    } catch (ignored) {}
    bot.Device.clearPointerMap();
    this.buttonPressed_ = null;
    this.elementPressed_ = null;
};

/**
 * A helper function to fire mouse double click events.
 *
 * @private
 */
bot.Mouse.prototype.maybeDoubleClickElement_ = function () {
    // Trigger an additional double click event if it is the second click.
    if (this.nextClickIsDoubleClick_) {
        this.fireMouseEvent_(bot.events.EventType.DBLCLICK);
    }
    this.nextClickIsDoubleClick_ = !this.nextClickIsDoubleClick_;
};

bot.Mouse.prototype.move = function (element, coords) {
    // If the element is interactable at the start of the move, it receives the
    // full event sequence, even if hidden by an element mid sequence.
    var toElemWasInteractable = bot.dom.isInteractable(element);

    var rect = bot.dom.getClientRect(element);
    this.clientXY_.x = coords.x + rect.left;
    this.clientXY_.y = coords.y + rect.top;
    var fromElement = this.getElement();

    if (element != fromElement) {
        // If the window of fromElement is closed, set fromElement to null as a flag
        // to skip the mouseout event and so relatedTarget of the mouseover is null.
        try {
            if (goog.dom.getWindow(goog.dom.getOwnerDocument(fromElement)).closed) {
                fromElement = null;
            }
        } catch (ignore) {
            // Sometimes accessing a window that no longer exists causes an error.
            fromElement = null;
        }

        if (fromElement) {
            // For the first mouse interaction on a page, if the mouse was over the
            // browser window, the browser will pass null as the relatedTarget for the
            // mouseover event. For subsequent interactions, it will pass the
            // last-focused element. Unfortunately, we don't have anywhere to keep the
            // state of which elements have been focused across Mouse instances, so we
            // treat every Mouse initially positioned over the documentElement or body
            // as if it's on a new page. Accordingly, for complex actions (e.g.
            // drag-and-drop), a single Mouse instance should be used for the whole
            // action, to ensure the correct relatedTargets are fired for any events.
            var isRoot = fromElement === bot.getDocument().documentElement || fromElement === bot.getDocument().body;
            fromElement = !this.hasEverInteracted_ && isRoot ? null : fromElement;
            this.fireMouseEvent_(bot.events.EventType.MOUSEOUT, element);
        }
        this.setElement(element);

        // All browsers except IE fire the mouseover before the mousemove.
        if (!goog.userAgent.IE) {
            this.fireMouseEvent_(bot.events.EventType.MOUSEOVER, fromElement, null, toElemWasInteractable);
        }
    }

    this.fireMouseEvent_(bot.events.EventType.MOUSEMOVE, null, null, toElemWasInteractable);

    // IE fires the mouseover event after the mousemove.
    if (goog.userAgent.IE && element != fromElement) {
        this.fireMouseEvent_(bot.events.EventType.MOUSEOVER, fromElement, null, toElemWasInteractable);
    }

    this.nextClickIsDoubleClick_ = false;
};

bot.Mouse.prototype.scroll = function (ticks) {
    if (ticks == 0) {
        throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR, 'Must scroll a non-zero number of ticks.');
    }

    // The wheelDelta value for a single up-tick of the mouse wheel is 120, and
    // a single down-tick is -120. The deltas in pixels (which is only relevant
    // for Firefox) appears to be -57 and 57, respectively.
    var wheelDelta = ticks > 0 ? -120 : 120;
    var pixelDelta = ticks > 0 ? 57 : -57;

    // Browsers fire a separate event (or pair of events in Gecko) for each tick.
    for (var i = 0; i < Math.abs(ticks); i++) {
        this.fireMouseEvent_(bot.events.EventType.MOUSEWHEEL, null, wheelDelta);
        if (goog.userAgent.GECKO) {
            this.fireMouseEvent_(bot.events.EventType.MOUSEPIXELSCROLL, null, pixelDelta);
        }
    }
};

bot.Mouse.prototype.fireMouseEvent_ = function (type, opt_related, opt_wheelDelta, opt_force, opt_count) {
    this.hasEverInteracted_ = true;
    if (bot.userAgent.IE_DOC_10) {
        var msPointerEvent = bot.Mouse.MOUSE_EVENT_MAP_[type];
        if (msPointerEvent) {
            // The pointerId for mouse events is always 1 and the mouse event is never
            // fired if the MSPointer event fails.
            if (
                !this.fireMSPointerEvent(
                    msPointerEvent,
                    this.clientXY_,
                    this.getButtonValue_(msPointerEvent),
                    bot.Device.MOUSE_MS_POINTER_ID,
                    MSPointerEvent.MSPOINTER_TYPE_MOUSE,
                    /* isPrimary */ true,
                    opt_related,
                    opt_force,
                )
            ) {
                return false;
            }
        }
    }
    return this.fireMouseEvent(type, this.clientXY_, this.getButtonValue_(type), opt_related, opt_wheelDelta, opt_force, null, opt_count);
};

bot.Mouse.prototype.getButtonValue_ = function (eventType) {
    if (!(eventType in bot.Mouse.MOUSE_BUTTON_VALUE_MAP_)) {
        return 0;
    }

    var buttonIndex = goog.isNull(this.buttonPressed_) ? bot.Mouse.NO_BUTTON_VALUE_INDEX_ : this.buttonPressed_;
    var buttonValue = bot.Mouse.MOUSE_BUTTON_VALUE_MAP_[eventType][buttonIndex];
    if (goog.isNull(buttonValue)) {
        throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR, 'Event does not permit the specified mouse button.');
    }
    return buttonValue;
};

bot.Mouse.prototype.getState = function () {
    return {
        buttonPressed: this.buttonPressed_,
        elementPressed: this.elementPressed_,
        clientXY: { x: this.clientXY_.x, y: this.clientXY_.y },
        nextClickIsDoubleClick: this.nextClickIsDoubleClick_,
        hasEverInteracted: this.hasEverInteracted_,
        element: this.getElement(),
    };
};
